use std::hash::Hash;

use dir_writer::{
    FileCollector, GeneratorArgs, IntermediateRepr, LanguageFeatures, RemoveDirBehavior,
};
use indexmap::IndexMap;
use serde::Serialize;

use crate::{generate_types::OpenApiUserData, r#type::TypeOpenApi};

pub mod builtin_schemas;
pub mod generate_types;
pub mod r#type;

#[derive(Default)]
pub struct OpenApiLanguageFeatures;

/// Fully render OpenAPI schema from IR to file output.
impl LanguageFeatures for OpenApiLanguageFeatures {
    fn name() -> &'static str {
        "openapi"
    }

    /// OpenAPI codegen creates a lot of files that BAML can't know about in
    /// advance, so we allow removing them.
    const REMOVE_DIR_BEHAVIOR: RemoveDirBehavior = RemoveDirBehavior::Unsafe;

    const CONTENT_PREFIX: &'static str = r#"
###############################################################################
#
#  Welcome to Baml! To use this generated code, please run the following:
#
#  $ openapi-generator generate -i openapi.yaml -g <language> -o <output_dir>
#
###############################################################################

# This file was generated by BAML: please do not edit it. Instead, edit the
# BAML files and re-generate this code.
    "#;

    fn generate_sdk_files(
        &self,
        collector: &mut FileCollector<Self>,
        ir: std::sync::Arc<IntermediateRepr>,
        _args: &GeneratorArgs,
    ) -> Result<(), anyhow::Error> {
        let user_data = OpenApiUserData::from_ir(ir.as_ref());
        let rendered = serde_yaml::to_string(&user_data.render())?;
        collector.add_file("openapi.yaml", rendered)?;
        collector.add_file(".openapi-generator-ignore", r#".gitignore"#)?;
        Ok(())
    }

    const GITIGNORE: Option<&'static str> = Some(
        r#"
###############################################################################
#
#  Welcome to Baml!
#
#  This .gitignore is here to keep you from accidentally checking in your
#  generated OpenAPI client - we strongly suggest you generate it at build time
#  instead.
#
#  Do not edit this file! BAML will overwrite all changes to this file.
#
#  If you do need to edit it, let us know: https://docs.boundaryml.com/contact
#
###############################################################################

# Ignore everything in this dir (because it's autogenerated)
*

# Except this .gitignore file
!.gitignore

# Preserving changes to .openapi-generator-ignore is also important
!.openapi-generator-ignore
"#,
    );
}

/// The top-level schema for the OpenAPI yaml file.
/// This type is meant to model the rendered format as
/// closely as possible. We render it
/// with serde_yaml::to_string.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct OpenApiSchema {
    pub openapi: String,
    pub info: serde_json::Value,
    pub servers: serde_json::Value,
    pub paths: IndexMap<String, IndexMap<String, Path>>,
    pub components: Components,
}

impl OpenApiSchema {
    pub fn from_ir(ir: &IntermediateRepr) -> Self {
        let user_data = OpenApiUserData::from_ir(ir);
        user_data.render()
    }
}

/// Format of the items in the `paths` field.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Path {
    /// The schema of the request body for this endpoint.
    pub request_body: PathRequestBody,
    /// Mapping from status code to response data.
    pub responses: IndexMap<&'static str, Response>,
    pub operation_id: String,
}

/// The responses of the paths.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Response {
    pub description: String,
    pub content: IndexMap<String, MediaTypeSchema>,
}

impl Hash for Response {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        let s = serde_yaml::to_string(self).expect("Should serialize");
        s.hash(state);
    }
}

#[derive(Debug, Clone, PartialEq, Hash, Eq, Serialize)]
pub struct FunctionName(String);

#[derive(Debug, Clone, PartialEq, Hash, Eq, Serialize, PartialOrd, Ord)]
pub struct TypeName(String);

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Components {
    #[allow(non_snake_case)]
    pub request_bodies: IndexMap<FunctionName, ComponentRequestBody>,
    pub schemas: IndexMap<TypeName, TypeOpenApi>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ComponentRequestBody {
    required: bool,
    content: IndexMap<String, MediaTypeSchema>,
}

/// A schema found under a media type in a `path` or 'content' field.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MediaTypeSchema {
    /// The schema of the return type of a function.
    schema: TypeOpenApi,
}

impl Hash for Components {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        let s = serde_yaml::to_string(self).expect("Should serialize");
        s.hash(state);
    }
}

#[derive(Debug, Clone, PartialEq, Hash, Eq, Serialize)]
pub struct PathRequestBody {
    #[serde(rename = "$ref")]
    pub ref_: String,
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use dir_writer::{FileCollector, GeneratorArgs};
    use internal_baml_core::ir::repr::make_test_ir;

    use super::*;

    #[test]
    fn test_gitignore_and_openapi_generator_ignore_files() {
        // Create a test IR with a simple function
        let ir = make_test_ir(
            r##"
            class Foo {
                age int
            }

            function TestFunction(name: string) -> Foo {
                client GPT4
                prompt #"Test prompt"#
            }

            client<llm> GPT4 {
                provider openai
                options {
                    model gpt-4
                    api_key env.OPENAI_API_KEY
                }
            }
            "##,
        )
        .expect("Valid IR");

        // Create FileCollector and generate files
        let lang_features = OpenApiLanguageFeatures;
        let mut collector = FileCollector::<OpenApiLanguageFeatures>::new();

        let args = GeneratorArgs {
            output_dir_relative_to_baml_src: PathBuf::from("generated"),
            baml_src_dir: PathBuf::from("."),
            inlined_file_map: Default::default(),
            version: "0.1.0".to_string(),
            no_version_check: false,
            default_client_mode:
                internal_baml_core::configuration::GeneratorDefaultClientMode::Sync,
            on_generate: vec![],
            client_type: internal_baml_core::configuration::GeneratorOutputType::OpenApi,
            client_package_name: None,
            module_format: None,
            is_pydantic_2: None,
        };

        // Generate SDK files
        lang_features
            .generate_sdk_files(&mut collector, std::sync::Arc::new(ir), &args)
            .unwrap();

        // Commit files to a temporary directory (this automatically adds .gitignore)
        let temp_dir = tempfile::tempdir().unwrap();
        let result = collector.commit(temp_dir.path()).unwrap();

        // Verify that all expected files are generated
        let expected_files = ["openapi.yaml", ".openapi-generator-ignore", ".gitignore"];

        for expected_file in &expected_files {
            let file_path = PathBuf::from(expected_file);
            assert!(
                result.contains_key(&file_path),
                "Generated files should contain {expected_file}"
            );
        }

        // Verify .gitignore content
        let gitignore_content = result.get(&PathBuf::from(".gitignore")).unwrap();
        assert!(
            gitignore_content.contains("Welcome to Baml!"),
            "Gitignore should contain BAML header"
        );
        assert!(
            gitignore_content.contains("Ignore everything in this dir"),
            "Gitignore should contain ignore pattern comment"
        );
        assert!(
            gitignore_content.contains("*\n"),
            "Gitignore should contain wildcard ignore pattern"
        );
        assert!(
            gitignore_content.contains("!.gitignore"),
            "Gitignore should preserve itself"
        );
        assert!(
            gitignore_content.contains("!.openapi-generator-ignore"),
            "Gitignore should preserve .openapi-generator-ignore"
        );

        // Verify .openapi-generator-ignore content
        let ignore_content = result
            .get(&PathBuf::from(".openapi-generator-ignore"))
            .unwrap();
        assert_eq!(
            ignore_content.trim(),
            ".gitignore",
            ".openapi-generator-ignore should contain .gitignore"
        );

        // Verify openapi.yaml exists and contains expected content
        let openapi_content = result.get(&PathBuf::from("openapi.yaml")).unwrap();
        assert!(
            openapi_content.contains("openapi: 3.0.0"),
            "OpenAPI YAML should contain version"
        );
        assert!(
            openapi_content.contains("TestFunction"),
            "OpenAPI YAML should contain the test function"
        );

        // Test passed - all expected files were generated with correct content
    }
}
